package com.gitee.wsl.time.schedule.cron

import com.gitee.wsl.ext.map.getOrDefault
import com.gitee.wsl.lang.bits.BitSet

internal class CronFieldParser(private val fieldType: CronFieldType) {
    private val fieldName = fieldType.fieldName
    private val length = fieldType.length
    private val maxAllowedValue = fieldType.maxAllowedValue
    private val minAllowedValue = fieldType.minAllowedValue

    private fun isInteger(str: String): Boolean {
        try {
            str.toInt()
            return true
        } catch (ex: NumberFormatException) {
            return false
        }
    }

    private fun parseValue(token: String): Int {
        if (this.isInteger(token)) {
            return token.toInt()
        } else {
            if (fieldType == CronFieldType.MONTH)
                return MONTHS_NAMES.getOrDefault(token, -1)
            if (fieldType == CronFieldType.DAY_OF_WEEK)
                return DAYS_OF_WEEK_NAMES.getOrDefault(token, -1)
            return -1
        }
    }

    fun parse(token: String): BitSet {
        // This is when last day of the month is specified
        if (this.fieldType == CronFieldType.DAY_OF_WEEK) {
            if (token.length == 2 && token.endsWith("l")) {
                return this.parseLiteral(token.substring(0, 1))
            }
        } else if (this.fieldType == CronFieldType.DAY && ("l" == token)) {
            return fieldType.parseLastDayOfMonth()
        }
        if (token.indexOf(',') > -1) {
            val bitSet: BitSet = BitSet(this.length)
            val items = token.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
            for (item in items) {
                bitSet.or(this.parse(item))
            }
            return bitSet
        }

        if (token.indexOf('/') > -1) return this.parseStep(token)

        if (token.indexOf('-') > -1) return this.parseRange(token)

        if (token.equals("*", ignoreCase = true)) {
            return fieldType.parseAsterisk()
        }

        return this.parseLiteral(token)
    }

    private fun parseStep(token: String): BitSet {
        try {
            val tokenParts =
                token.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
            if (tokenParts.size != 2) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token)
                )
            }
            val stepSizePart = tokenParts[1]
            val stepSize = this.parseValue(stepSizePart)
            if (stepSize < 1) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token) +"minimum allowed step (every) value is \"1\""
                )
            }
            var numSetPart = tokenParts[0]
            if (!numSetPart.contains("-") && numSetPart != "*" && isInteger(numSetPart)) {
                // if number is a single digit, it should be a range starts with that
                // number and ends with the maximum allowed value for the field type
                numSetPart = "$numSetPart-${this.maxAllowedValue}"
            }
            val numSet: BitSet = this.parse(numSetPart)
            val stepsSet: BitSet = BitSet(this.length)
            var i: Int = numSet.nextSetBit(0)
            while (i < this.length) {
                stepsSet.set(i)
                i += stepSize
            }
            stepsSet.and(numSet)
            return stepsSet
        } catch (ex: NumberFormatException) {
            throw InvalidCronExpressionException(
                format(this.fieldName, token), ex
            )
        }
    }

    private fun parseRange(token: String): BitSet {
        val rangeParts = token.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
        if (rangeParts.size != 2) {
            throw InvalidCronExpressionException(
                format(this.fieldName, token)
            )
        }
        try {
            val from = this.parseValue(rangeParts[0])
            if (from < 0) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token)
                )
            }
            if (from < this.minAllowedValue) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token) + " minimum allowed value for ${this.fieldName} field is \" ${this.minAllowedValue} \""
                )
            }

            val to = this.parseValue(rangeParts[1])
            if (to < 0) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token)
                )
            }
            if (to > this.maxAllowedValue) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token) + " minimum allowed value for ${this.fieldName} field is \" ${this.maxAllowedValue} \""
                )
            }
            if (to < from) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token) + " the start of range value must be less than or equal the end value"
                )
            }
            return fieldType.fillBitSetToIncl(from, to)
        } catch (ex: NumberFormatException) {
            throw InvalidCronExpressionException(
                format(this.fieldName, token), ex
            )
        }
    }

    private fun parseLiteral(token: String): BitSet {
        val bitSet: BitSet = BitSet(this.length)
        try {
            val number = this.parseValue(token)
            if (number < 0) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token)
                )
            }
            if (number < this.minAllowedValue) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token) + " minimum allowed value for ${this.fieldName} field is \" ${this.minAllowedValue} \""
                )
            }
            if (number > this.maxAllowedValue) {
                throw InvalidCronExpressionException(
                    format(this.fieldName, token) + " maximum allowed value for ${this.fieldName} field is \" ${this.maxAllowedValue} \""
                )
            }
            fieldType.setBitSet(bitSet, number)
        } catch (ex: NumberFormatException) {
            throw InvalidCronExpressionException(
                format(this.fieldName, token), ex
            )
        }
        return bitSet
    }

    companion object {
        private const val INVALID_FIELD = "invalid %s field: \"%s\"."

        private fun format( one:Any, two:Any):String{
            return "invalid $one field: \" $two \"."
        }

        private val MONTHS_NAMES: MutableMap<String, Int> =
            mutableMapOf<String, Int>()
        private val DAYS_OF_WEEK_NAMES: MutableMap<String, Int> =
            mutableMapOf<String, Int>()

        init {
            MONTHS_NAMES["January"] = 1
            MONTHS_NAMES["Jan"] = 1
            MONTHS_NAMES["February"] = 2
            MONTHS_NAMES["Feb"] = 2
            MONTHS_NAMES["March"] = 3
            MONTHS_NAMES["Mar"] = 3
            MONTHS_NAMES["April"] = 4
            MONTHS_NAMES["Apr"] = 4
            MONTHS_NAMES["May"] = 5
            MONTHS_NAMES["June"] = 6
            MONTHS_NAMES["Jun"] = 6
            MONTHS_NAMES["July"] = 7
            MONTHS_NAMES["Jul"] = 7
            MONTHS_NAMES["August"] = 8
            MONTHS_NAMES["Aug"] = 8
            MONTHS_NAMES["September"] = 9
            MONTHS_NAMES["Sep"] = 9
            MONTHS_NAMES["October"] = 10
            MONTHS_NAMES["Oct"] = 10
            MONTHS_NAMES["November"] = 11
            MONTHS_NAMES["Nov"] = 11
            MONTHS_NAMES["December"] = 12
            MONTHS_NAMES["Dec"] = 12

            DAYS_OF_WEEK_NAMES["Sunday"] = 0
            DAYS_OF_WEEK_NAMES["Sun"] = 0
            DAYS_OF_WEEK_NAMES["Monday"] = 1
            DAYS_OF_WEEK_NAMES["Mon"] = 1
            DAYS_OF_WEEK_NAMES["Tuesday"] = 2
            DAYS_OF_WEEK_NAMES["Tue"] = 2
            DAYS_OF_WEEK_NAMES["Wednesday"] = 3
            DAYS_OF_WEEK_NAMES["Wed"] = 3
            DAYS_OF_WEEK_NAMES["Thursday"] = 4
            DAYS_OF_WEEK_NAMES["Thu"] = 4
            DAYS_OF_WEEK_NAMES["Friday"] = 5
            DAYS_OF_WEEK_NAMES["Fri"] = 5
            DAYS_OF_WEEK_NAMES["Saturday"] = 6
            DAYS_OF_WEEK_NAMES["sat"] = 6
        }
    }
}
